package httputil

import (
	"bytes"
	"crypto/tls"
	"crypto/x509"
	"errors"
	"fmt"
	"gmap-backend/internal/util/commonutil"
	"gmap-backend/internal/util/httpurlutil"
	"golang.org/x/text/encoding/simplifiedchinese"
	"io"
	"mime/multipart"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"
)

// UploadFileOption 文件上传选项
// MemBufThreadHold 超过指定值时，会把multipart的内容写入临时文件
// WorkDir 临时文件的目录
type UploadFileOption struct {
	Timeout          time.Duration
	Ua               string
	MemBufThreadHold int64
	WorkDir          string
	FileItemName     string
}

// UploadFileByBearerToken 上传文件，使用bearer认证方式
func UploadFileByBearerToken(localFilePath string, uploadUrl string, bearerToken string, option *UploadFileOption) (upResult bool, resultErr any, respBuf []byte) {
	defer func() {
		if err := recover(); nil != err {
			upResult = false
			resultErr = err
			respBuf = nil
		}
	}()

	// 打开文件准备后面读取
	stat, err := os.Stat(localFilePath)
	if nil != err || stat.IsDir() {
		panic(fmt.Sprintf("文件路径无效：%s", localFilePath))
	}
	fileReader, err := os.Open(localFilePath)
	throwIfErr(err)
	defer fileReader.Close()

	// 如果文件大小超过指定值，则使用临时文件保存multipart信息，否则使用内存保存
	fileSize := stat.Size()
	useTmpFileCache := fileSize > option.MemBufThreadHold
	var tmpFile *os.File
	var memBuf *bytes.Buffer
	tmpFilePath := filepath.Join(option.WorkDir, commonutil.GetUuid())
	if useTmpFileCache {
		tmpFile, err = os.OpenFile(tmpFilePath, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)
		throwIfErr(err)
	} else {
		memBuf = &bytes.Buffer{}
	}
	defer func() {
		if nil != tmpFile {

			tmpFile.Close()

			st, e := os.Stat(tmpFilePath)
			if nil == e {
				println("tmp file size", st.Size())
			}

			os.Remove(tmpFilePath)

			st, e = os.Stat(tmpFilePath)
			if nil != e {
				println("tmp file already disappear")
			} else {
				println("tmp file remain, size is ", st.Size())
			}
		}
	}()

	// 写入multipart信息
	var multiWriter *multipart.Writer
	if useTmpFileCache {
		multiWriter = multipart.NewWriter(tmpFile)
	} else {
		multiWriter = multipart.NewWriter(memBuf)
	}
	part, err := multiWriter.CreateFormFile(option.FileItemName, filepath.Base(localFilePath))
	throwIfErr(err)
	io.Copy(part, fileReader)
	multiWriter.Close()

	// 创建请求信息
	var req *http.Request
	if useTmpFileCache {
		// 重置当前位置以便读取
		tmpFile.Seek(0, 0)
		req, err = http.NewRequest("POST", uploadUrl, tmpFile)
		throwIfErr(err)
	} else {
		req, err = http.NewRequest("POST", uploadUrl, memBuf)
		throwIfErr(err)
	}
	req.Header.Set("User-Agent", option.Ua)
	req.Header.Set("Authorization", "Bearer "+bearerToken)
	req.Header.Set("Content-Type", multiWriter.FormDataContentType())

	// 发送请求
	client := getClient(option.Timeout)
	resp, err := client.Do(req)
	throwIfErr(err)
	defer resp.Body.Close()

	// 响应结果
	respSucc := resp.StatusCode >= 200 && resp.StatusCode <= 299
	if !respSucc {
		panic(fmt.Sprintf("响应状态异常: %d", resp.StatusCode))
	}
	bytes := readBodyBytes(resp.Body)
	return true, nil, bytes
}

type DownloadResult struct {
	Succ        bool
	SaveToMem   bool
	ContentType string
	CnEncode    bool
	Content     []byte
	SavePath    string
	FactUrl     string
}

// DownloadToMem 下载指定url内容到内存
func DownloadToMem(url string, timeout time.Duration, ua string) (downResult *DownloadResult) {
	return baseDownloadInner(url, true, "", timeout, ua)
}

// Download 下载指定url内容到文件
func Download(url string, savePath string, timeout time.Duration, ua string) (downResult *DownloadResult) {
	return baseDownloadInner(url, false, savePath, timeout, ua)
}

func baseDownloadInner(url string, saveToMem bool, savePath string, timeout time.Duration, ua string) (downResult *DownloadResult) {
	defer func() {
		if err := recover(); nil != err {
			downResult = &DownloadResult{
				Succ:        false,
				ContentType: "",
				CnEncode:    false,
			}
		}
	}()

	client := getClient(timeout)
	req, err := http.NewRequest("GET", url, nil)
	throwIfErr(err)
	req.Header.Add("User-Agent", ua)
	resp, err := client.Do(req)
	throwIfErr(err)
	defer func() {
		if nil != resp && nil != resp.Body {
			resp.Body.Close()
		}
	}()

	if resp.StatusCode >= 300 && resp.StatusCode <= 399 {
		redirectUrl, err := resp.Location()
		throwIfErr(err)
		redirectFullUrl := httpurlutil.CombineUrl(url, httpurlutil.GetRootUrl(url), redirectUrl.Path)
		return baseDownloadInner(redirectFullUrl, saveToMem, savePath, timeout, ua)
	}

	respSucc := resp.StatusCode >= 200 && resp.StatusCode <= 299
	if !respSucc {
		panic("resp status error")
	}
	if nil == resp.Body {
		panic("not response body")
	}

	contentType := strings.TrimSpace(strings.ReplaceAll(strings.ToLower(resp.Header.Get("Content-Type")), " ", ""))
	isChineseEncoding := false
	if strings.Contains(contentType, "charset=gb2312") || strings.Contains(contentType, "charset=gbk") || strings.Contains(contentType, "charset=gb18030") {
		isChineseEncoding = true
	}

	var buf bytes.Buffer
	if saveToMem {
		savePath = ""
		writeBodyToMem(resp.Body, &buf)
	} else {
		if 0 == writeBodyToFile(resp.Body, savePath) {
			panic("resp body length 0")
		}
	}

	return &DownloadResult{
		Succ:        true,
		SaveToMem:   saveToMem,
		ContentType: contentType,
		CnEncode:    isChineseEncoding,
		FactUrl:     url,
		SavePath:    savePath,
		Content:     buf.Bytes(),
	}
}

type HtmlContResult struct {
	Succ    bool
	Content string
	FactUrl string
}

// GetHtmlContentWithRedirect 获取html内容，支持重定向
func GetHtmlContentWithRedirect(url string, timeout time.Duration, ua string) (retResult *HtmlContResult) {
	defer func() {
		if err := recover(); nil != err {
			retResult = &HtmlContResult{
				Succ:    false,
				Content: "",
				FactUrl: "",
			}
		}
	}()

	result := DownloadToMem(url, timeout, ua)
	if !result.Succ {
		panic("get html content err")
	}

	// 如果是中文字符集，则需要转换处理
	if result.CnEncode {
		utf8Bytes, err := simplifiedchinese.GB18030.NewDecoder().Bytes(result.Content)
		throwIfErr(err)
		retResult = &HtmlContResult{
			Succ:    true,
			Content: string(utf8Bytes),
			FactUrl: result.FactUrl,
		}
	}

	return &HtmlContResult{
		Succ:    true,
		Content: string(result.Content),
		FactUrl: result.FactUrl,
	}
}

// 获取客户端，不验证https证书
func getClient(timeout time.Duration) *http.Client {
	client := &http.Client{
		Timeout: timeout,
		Transport: &http.Transport{
			TLSClientConfig: &tls.Config{
				InsecureSkipVerify: true,
				VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
					return nil
				},
			},
		},
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			return http.ErrUseLastResponse
		},
	}
	return client
}

func readBodyBytes(respBody io.ReadCloser) []byte {
	result := make([]byte, 0, 8192)
	readBodyInner(respBody, func(buf []byte, size int) {
		result = append(result, buf[:size]...)
	})
	return result
}

func writeBodyToMem(respBody io.ReadCloser, bufWriter *bytes.Buffer) int {
	sumSize := 0
	readBodyInner(respBody, func(buf []byte, size int) {
		bufWriter.Write(buf[:size])
		sumSize += size
	})
	return sumSize
}

func writeBodyToFile(respBody io.ReadCloser, savePath string) int {
	sumSize := 0
	file, err := os.Create(savePath)
	if nil != err {
		panic("cannot save http respone to file")
	}
	readBodyInner(respBody, func(buf []byte, size int) {
		file.Write(buf[:size])
		sumSize += size
	})
	if nil != file.Close() {
		panic("cannot save http respone to file")
	}
	return sumSize
}

func readBodyInner(respBody io.ReadCloser, callback func(buf []byte, size int)) {
	buffer := make([]byte, 8192)
	for {
		size, err := respBody.Read(buffer)
		if nil != err && !errors.Is(err, io.EOF) {
			panic("read resp error")
		}
		if 0 == size {
			break
		}
		if nil != callback {
			callback(buffer, size)
		}
		if nil != err && errors.Is(err, io.EOF) {
			break
		}
	}
}

func throwIfErr(err error) {
	if nil != err {
		panic(err)
	}
}
